<?php

class Yz_Tree_Default
    extends Yz_Class
    implements ArrayAccess, Countable, IteratorAggregate, Yz_Array_Interface, Yz_Tree_Interface
{

    protected $rootElement;
    protected $tree = array();

    public function __construct()
    {
        debug();
    }


    /** toString()
    *
    *   @return string
    */
    public function __toString()
    {
        $cl = $this->className();
        $count = $this->count();
        return "Object({$cl}[{$count}])";
    } // toString

    /**
     *  apply callback recursively to each element of the tree (i.e. all descendants of the element)
     *  Note: the processing order depends on implementation
     */
    public function apply($callback)
    {
        debug();

        if( ! is_callable( $callback ) ) {
            throw new Yz_Exception_InvalidArgument();
        }
        return $this->_applyToElement($callback, $this->rootElement);
    }

    protected function _applyToElement($callback, $el)
    {
        call_user_func( $callback, $el );
        foreach ($el->children() as $ch) {
            $this->_applyToElement($callback, $ch);
        }
        return $this;
    }

    public function createElement($id, $value)
    {
        debug();
        $id = $this->adjustIndex($id);
        if (array_key_exists($id, $this->tree)) {
            throw new Yz_Exception("Element with id=$id already exists");
        }
        $el = new Yz_Tree_Element($this, $id, $value);
        if (null === $this->rootElement) {
            $this->rootElement = $el;
        }
        $this->tree[$id] = $el;
        return $el;
    }

    /**
    *   @return bool
    */
    public function elementExists( Yz_Tree_Element_Interface $el )
    {
        debug();
        $id = $el->id();
        return array_key_exists($id, $this->tree)
            && $this->tree[$id] === $el;
    }

    public function getIterator()
    {
        return $this->rootElement;
    }

    public function getRootElement()
    {
        return $this->rootElement;
    }

    /**
     *  Each element of the input array should contain at least the id property, and parentId if necessary
     *  The order of elements in the input array must be such that elements' parents be known before
     *      the elements themselves. Otherwise, an element's parentId property will be ignored.
     *
     *  @throw Exception if element.id or element.parentId fail to validate as possible tree element IDs
     *  @throw Exception if input can not be converted to an array
     */
    public static function fromArray($array)
    {
        $input = Yz_Array::valueOf($array);
        $tree = new static();
        foreach ($input as $entry) {
            if (isset($entry['id'])) {
                $el = $tree->createElement($entry['id'], $entry);
                if ( isset($entry['parentId']) ) {
                    $parent = $tree->offsetGet($entry['parentId']);
                    if (null !== $parent) {
                        $parent->addChild($el);
                    }
                }
            }
        }
        return $tree;
    }

    /** isEmpty()
    *   @return bool
    */
    public function isEmpty()
    {
        return 0 === count( $this->tree );
    } // isEmpty


    /**
    *  Countable method
    *   @return int
    */
    public function count()
    {
        return count( $this->tree );
    }


    /**
    *  Alias for count()
    *   @return int
    */
    public function length()
    {
        return $this->count();
    }

    /*******************************
    ****   ArrayAccess methods   ***
    *******************************/

    /**
     *  apply callback recursively to each element of the tree (i.e. all descendants of the element)
     *      and obtain the result value
     *  Note: the processing order depends on implementation
     *  @param callback $callback : function(element, value) => newValue
     *  @param mixed(null) initial value
     *  @return mixed result value
     */
    public function reduce($callback, $initialValue=null)
    {
        debug();

        if( ! is_callable( $callback ) ) {
            throw new Yz_Exception_InvalidArgument();
        }
        return $this->_reduceElement($callback, $this->rootElement, $initialValue);
    }

    protected function _reduceElement($callback, $el, $value)
    {
        $value = call_user_func( $callback, $el, $value );
        foreach ($el->children() as $ch) {
            $value = $this->_reduceElement($callback, $ch, $value);
        }
        return $value;
    }

    public function toArray()
    {
        debug();
        return $this->tree;
    }

    protected function adjustIndex($index)
    {
        debug();
        return Yz_Int::testPositive($index);
    }

    /** test if the index exists in Tree
     *  @param string index (after adjustIndex)
     *  @return bool
     */
    protected function indexExists( $index )
    {
        return array_key_exists( $index, $this->tree );
    }

    /** the default getter is called after index verification
     *  @param string index
     *  @return mixed|null
     */
    protected function defaultGetter( $index )
    {
        if ($this->indexExists( $index )) {
            return $this->tree[$index];
        }
        return null;
    }

    /** the default setter is called after index verification
     *  @param string index
     *  @return chainable
     */
    protected function defaultSetter( $index, $value )
    {
        throw new Yz_Exception("Can not set values directly");
    }

    /** the default unsetter is called after index verification
     *  @param string index
     *  @return chainable
     */
    protected function defaultUnsetter( $index )
    {
        throw new Yz_Exception("Can not unset values directly");
    }

}
